<template>
    <audio id="audio" preload="none"></audio>
    <div class="ai-container" :class="device.device">
        <img src="./imgs/logo.png" class="logo">
        <div id="video-box" :class="{scale: [2,3,4,5,6,10].indexOf(TYPE) > -1}"></div>
        <div class="bottom-content">
            <div class="content-box">
                <div class="scroll-box" id="scroll-box">
                    <div class="scroll-box-content" id="scroll-box-content">
                        <div class="message-item" :class="'type-' + item.type " v-for="(item, i) in messages" :key="i">
                            <i class="msg-icon" :class="{2: 'gray', 1: 'green', 3: 'yellow'}[item.type]"></i>
                            <p class="con" v-html="item.content"></p>
                        </div>
                        <div class="loader-inner ball-pulse" v-show="isLoading && disabledSend">
                            <div></div>
                            <div></div>
                            <div></div>
                        </div>
                    </div>
                </div>
            </div>

            <div id="qs-container" class="qs-container" v-show="isChart && isQSShow && question.length > 0" style="opacity: 0">
                <div class="qs-view">
                    <span @click="onQSClick(item, i)" class="qs-item" v-for="(item, i) in question" :key="i">{{ (i + 1) + '. ' + item }}</span>
                </div>
            </div>

            <div class="hj-button-box" v-show="isChart">
                <div class="waveview" id="waveview" style="opacity: 0"></div>
                <div class="hungup" v-if="isLoading && disabledSend" @click="onCancel" style="color: #fff">取消</div>
                <div class="hungup" v-else-if="isSpeekBtn" @click="onCancelSpeek" style="color: #fff">停止</div>
                <div class="hungup" v-else @click="onStopCall"><img src="./imgs/hungup.png"></div>
                <div class="hj-button dui-hua" :class="{disabled: disabledSend}" @click="onDuiHua">{{ isRecording ? '发送对话' : '开始对话' }}</div>
            </div>
            <div class="hj-button-box" v-show="!isChart">
                <div class="hj-button" @click="OnHujiaoClick" style="margin-left: 0;">呼叫{{TYPE == 1 ? '老师' : '顾问'}} {{ teacher.name }}</div>
            </div>
        </div>

        <!--       全屏呼叫老师 -->
        <div class="call-box" v-show="isCall">
            <div class="call-img-box">
                <img class="call-img" :src="teacher.logo" alt="">
                <span>正在呼叫 {{ teacher.name }} {{ labICalling }}</span>
            </div>
            <img src="./imgs/call.png" style="width: 80px;" @click="onStopCall">
        </div>
    </div>

</template>

<script setup>
import {ref, onMounted, watch} from 'vue';
import Recorder from 'recorder-core/recorder.mp3.min'
import {getParams, Speakvideo, icalling, getDevice} from './utils';
import {getToken, getAudioUrl, getAudio, ask, audioToText} from './utils/api.js';
import {cancel} from './utils/request.js';
import OSSUpload from './utils/UploadOSS.js'
import WaveView from './utils/waveview.js'
import call from './call.mp3'
import logo1 from "@/videos/logo1.png";//默认
import logo2 from "@/videos/logo2.png";//孙涛
import logo3 from "@/videos/logo3.png";//李璐
import logo4 from "@/videos/logo4.png";//卓尔
import logo5 from "@/videos/logo5.png";//都琳
import logo6 from "@/videos/logo6.png";//董琳雅
import logo10 from "@/videos/logo10.png";//欣慰

const labICalling = ref('')
const isCall = ref(false)
const messages = ref([])
const isChart = ref(false)
const isRecording = ref(false)
const isSpeekBtn = ref(false)
const disabledSend = ref(false)
const isLoading = ref(false)
const isQSShow = ref(false)
const teacher = ref({})
const device = ref(getDevice())

/** 相关性问题 (学生在报名学校时，最关心的问题有：)*/
const questionListTea = ['报名的具体流程是什么？', '报名需要准备哪些材料？', '报名费用是多少？', '报名截止日期是什么时候？', '报名后需要参加什么样的入学测试？', '报名成功后如何办理入学手续？', '是否可以选择线上报名？', '报名后如何查询报名结果？', '报名后有没有相关的奖学金政策？', '报名时是否需要提供推荐信或推荐人？']
const questionListCom = ['校校是什么？', '校校的发展历程有哪些？', '校校核心技术是什么？', 'AI招生老师是什么？', 'AI智能批改是什么？', 'AI口语考官怎么用？', '教务如何智能排课？', 'AI口语考官怎么用？', '学生如何自主选课？', '智能选校提供哪些升学数据？']

let scrollBox = null,
    scrollBoxCon = null,
    token = null,
    speak_video = null,
    callTimer = null,
    waveView = null,
    waveViewEl = null,
    ws = null,
    recAuth = null,
    videoBox = null,
    errorTotalMin = 3,
    status,
    audio,
    recorder = null, blob = null, audioStream = null, isRecordReady = false;

let params = getParams('post')
let TeacherType = parseInt( params.type || 1);

const COM_TYPE_LIST = [10,2,3,4,5,6]
if(location.href.indexOf('com.html') > -1 || location.href.indexOf('localhost') > -1){
    if(COM_TYPE_LIST.indexOf(TeacherType) < 0){
        TeacherType = COM_TYPE_LIST[0]
    }
}

const CEA_TYPE_LIST = [1]
if(location.href.indexOf('tea.html') > -1 || params.type == 'tea'){
    TeacherType = CEA_TYPE_LIST[0]
}

const TYPE = ref(TeacherType)

let questionList = TeacherType === 1 ?  [...questionListTea] : [...questionListCom]
let question = ref([])

/**
 * AI 老师
 * */
const TEACHERS = {
    1: {name: 'Joanna', content: '你好，我是校校国际学校的招生老师（Joanna）。我可以解答关于学校报名和学校信息以及你的规划方面的问题，都可以随时问我！', logo: logo1},
    2: {name: 'Sean', content: '你好，我是校校的AI顾问 Sean，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo2}, // 孙超
    3: {name: 'Lucia', content: '你好，我是校校的AI顾问 Lucia，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo3}, // 李璐
    4: {name: 'Joey', content: '你好，我是校校的AI顾问 Joey，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo4}, // 卓尔
    5: {name: 'Doreen', content: '你好，我是校校的AI顾问 Doreen，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo5}, // 都琳
    6: {name: 'Lucy', content: '你好，我是校校的AI顾问 Lucy，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo6}, // 董琳雅
    10: {name: 'Sinva', content: '你好，我是校校的AI顾问 Sinva，关于校校的产品和使用方面的问题，都可以随时问我哦！', logo: logo10}, // 欣慰
}

teacher.value = TEACHERS[TeacherType]

onMounted(() => {
    scrollBox = document.getElementById('scroll-box')
    waveViewEl = document.getElementById('waveview')
    scrollBoxCon = document.getElementById('scroll-box-content')
    videoBox = document.getElementById('video-box')
    speak_video = new Speakvideo(videoBox, TeacherType)
    videoBox.style.background = `url(${speak_video.gif})`
    videoBox.style.marginTop = speak_video.typeTimeData.top || 0
})

watch(disabledSend, (val) => {
    isQSShow.value = !val
})
watch(isRecording, (val) => {
    isQSShow.value = !val
})

function randomUrl(url){
    url = url + '?t=' + Date.now()
    return url
}

//呼叫
function OnHujiaoClick() {
    messages.value = [];
    question.value = questionList
    isCall.value = true
    audio = document.getElementById('audio')
    audio.src = randomUrl(call)
    audio.loop = true
    audio.play()
    audio.onplaying = function () {}
    icalling.start((val) => {
        labICalling.value = val
    })
    callTimer = setTimeout(() => {
        audio.pause()
        audio.loop = false
        isCall.value = false
        isChart.value = true
        getAudioUrl().then((res) => {
            audio.src = randomUrl(res.data || call)
        })
        readToken()
        setTimeout(() => {
            document.getElementById('qs-container').style.opacity = 1
        },2000)
    }, 4200)
}

//停止拨打, 退出到主页
function onStopCall() {
    if (callTimer) {
        clearTimeout(callTimer)
    }
    messages.value = []
    isCall.value = false
    isChart.value = false
    disabledSend.value = true
    onCancelSpeek()
    cancelAsk()
    icalling.stop()
    if (recorder) {
        recorder.stop(() => {
            recorder.close();//释放录音资源
            isRecording.value = false
        }, function (msg) {
        })
    }
}

function readToken() {
    getToken().then((res) => {
        token = res.data
        onSend({text: TEACHERS[TeacherType].content, type: 2}, )
    }).catch((error) => {
        if (error && error.errorTotal < errorTotalMin) {
            readToken()
        } else {
            alert('系统初始化失败，请刷新~')
            onStopCall()
        }
    })
}

function addMessage(msg, type, bool) {
    if(!isChart.value)return
    if (bool) messages.value = [];
    messages.value.push({type: type, content: formatImgStr(msg)})
    setTimeout(() => {
        if (scrollBox) {
            scrollBox.scrollTop = scrollBox.scrollHeight
        }
    }, 200)
}

/**
 [IMG/A]![一串中文或英文](网址URL)
 解析：一串中文或英文 + 换行 + 图片显示

 [A]![一串中文或英文](网址URL)
 解析：一串中文或英文 + 换行 + 网址：URL
 * */
function formatImgStr(str){
    let f = str.match(/\[IMG\/A]!\[.*?]\(.*?\)/g)
    if(f && f.length > 0){
        for(let i = 0; i < f.length; i++){
            let t = f[i];
            let c = /!\[(.*?)]/.exec(t)
            if(c)c = c[1]
            let u = /\((.*?)\)/.exec(t)
            if(u)u = u[1]
            str = str.replace(t, `${c}<br><img src="${u}" style="width: 120px;border-radius: 4px;margin-top: 10px"><br><br>`)
        }
    }
    let s = str.match(/\[A]!\[.*?]\(.*?\)/g)
    if(s && s.length > 0){
        for(let i = 0; i < s.length; i++){
            let t = s[i];
            let c = /!\[(.*?)]/.exec(t)
            if(c)c = c[1]
            let u = /\((.*?)\)/.exec(t)
            if(u)u = u[1]
            str = str.replace(t, `${c}<br>链接：<a href="${u}" target="_blank" style="color: rgb(70 255 255);">点击查看</a>`)
        }
    }
    str = str.replace(/(笑笑|小小|潇潇|晓晓|肖肖|萧萧|筱筱)/, '校校')
    return str
}

//语音转文字去掉中间的url
function checkContent(str){
    let f = str.match(/\[IMG\/A]!\[.*?]\(.*?\)/g)
    if(f && f.length > 0){
        for(let i = 0; i < f.length; i++){
            let t = f[i];
            let c = /!\[(.*?)]/.exec(t)
            if(c)c = c[1]
            let u = /\((.*?)\)/.exec(t)
            if(u)u = u[1]
            str = str.replace(t, c + '\n\n' + '长按识别二维码或手机微信扫一扫可以访问哦~')
        }
    }
    let s = str.match(/\[A]!\[.*?]\(.*?\)/g)
    if(s && s.length > 0){
        for(let i = 0; i < s.length; i++){
            let t = s[i];
            let c = /!\[(.*?)]/.exec(t)
            if(c)c = c[1]
            let u = /\((.*?)\)/.exec(t)
            if(u)u = u[1]
            str = str.replace(t, c + '\n\n' + '点击查看可以访问哦~')
        }
    }
    str = str.replace('www.xiaoxiao.com', 'www.xiaosaas.com')
    if(str.indexOf('www.xiaosaas.com') > -1){
        str = str.replace('www.xiaosaas.com', 'www点校撒似点com')
    }
    return str
}

let isSendFrom = false;
function onSend(opt, isSend) {
    return new Promise((resolve) => {
        isSendFrom = true
        disabledStartSpeek()
        getAudio(checkContent(opt.text), TeacherType).then((data) => {
            audio.src = randomUrl(data)
            audio.load()
            audio.oncanplay = function (){
                audio.play()
            }
            audio.onplaying = function () {
                if (isSend !== false && isSendFrom) {
                    showSpeekStopBtn()
                    addMessage(opt.text, opt.type)
                    isSendFrom = false
                }
                speak_video.startSpeak()
            }
            audio.onended = function () {
                audio.src = ''
                showStartSpeek()
                speak_video.endSpeak()
                resolve()
            }
        }).catch((error) => {
            if (error === 'ERR_CANCELED') {
                return
            }
            if (error && error.errorTotal < errorTotalMin) {
                readMessage({text: '请稍等下，正在努力为你解答！', type: 2}).then(() => {
                    onSend(opt, isSend)
                })
            } else {
                readMessage({text: '非常抱歉，老师还不能帮助你更多！', type: 2}).then(() => {
                    showStartSpeek()
                })
            }
        })
    })
}

function readMessage(opt, isSend) {
    return new Promise((resolve, reject) => {
        if (isSend !== false) {
            addMessage(opt.text, opt.type)
        }
        getAudio(opt.text, TeacherType).then((data) => {
            audio.src = randomUrl(data)
            audio.load()
            audio.oncanplay = function (){
                audio.play()
            }
            audio.onplaying = function () {
                speak_video.startSpeak()
            }
            audio.onended = function () {
                audio.src = ''
                speak_video.endSpeak()
                resolve()
            }
        }).catch((error) => {
            if (error === 'ERR_CANCELED') {
                return
            }
            showStartSpeek()
            if (error && error.errorTotal < 4) {
                readMessage(opt, isSend).then(resolve).catch(reject)
            } else {
                reject()
            }
        })
    })
}

function onDuiHua() {
    if (disabledSend.value) {
        return
    }
    if (isRecording.value) {
        stopRecording()
    } else {
        startRecording()
    }
}

function onQSClick(text, i) {
    question.value.splice(i, 1)
    disabledSend.value = true
    isLoading.value = true
    addMessage(text, 1)
    sendAsk(text)
}

/************************* recorder ************************************/
function initRec() {
    return new Promise((resolve, reject) => {
        recorder = Recorder({
            type: 'mp3',
            sampleRate: 16000, //采样频率，默认为44100Hz(标准MP3采样率)
            bitRate: 16, //比特率，默认为  128kbps(标准MP3质量)
            error: reject,
            fix: reject,
            onProcess: (buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) => {
                waveView.input(buffers[buffers.length - 1], powerLevel, bufferSampleRate);
            }
        })
        recorder.open(() => {
            isRecordReady = true
            resolve()
        }, () => {
            isRecording.value = false
            waveViewEl.style.opacity = 0
            readMessage({text: '需要授权录音权限，才能提问哦~', type: 2}, recAuth ? false : null)
            recAuth = true
        })
    })
}

function startRecording() {
    if (isRecording.value) return;
    isRecording.value = true;
    waveView = WaveView({elem: ".waveview"});
    waveViewEl.style.opacity = 1
    initRec().then(() => {
        recorder.start()
    })
}

function stopRecording() {
    if (!recorder || !isRecordReady || !isRecording.value) {
        return
    }
    isRecording.value = false
    waveViewEl.style.opacity = 0
    recorder.stop((blob) => {
        //释放录音资源
        recorder.close()
        recorder = null
        disabledStartSpeek()
        showSendStopBtn()
        recOver(blob)
    }, function () {
        showStartSpeek()
        readMessage({text: '非常抱歉，录音存在一点点问题，能重新录下吗', type: 2})
    })
}

function recOver(blob) {
    let up = new OSSUpload();
    up.upload(blob).then((res) => {
        if (res.success) {
            getRecText(res.url)
        } else {
            readMessage({text: '请稍等下，正在努力为你解答！', type: 2}).then(() => {
                recOver(blob)
            })
        }
    }).catch(() => {
        readMessage({text: '请稍等下，正在努力为你解答！', type: 2}).then(() => {
            recOver(blob)
        })
    })
}

function getRecText(url) {
    audioToText(url).then((text) => {
        if (text) {
            addMessage(text, 1)
            sendAsk(text)
        } else {
            readMessage({text: '内容有点少，可以多说一点点吗？', type: 2})
            hideLoading()
            showStartSpeek()
        }
    }).catch((error) => {
        if (error === 'ERR_CANCELED') {
            return
        }
        if (error && error.errorTotal < errorTotalMin) {
            readMessage({text: '请稍等下，正在努力为你解答！', type: 2}).then(() => {
                getRecText(url)
            })
        } else {
            readMessage({text: '非常抱歉，老师还不能帮助你更多！', type: 2}).then(() => {
                showStartSpeek()
            })
        }
    })
}

function sendAsk(text) {
    ask(text, TeacherType).then((data) => {
        onSend({text: data, type: 2})
    }).catch((error) => {
        if (error === 'ERR_CANCELED') {
            return
        }
        if (error && error.errorTotal < errorTotalMin) {
            readMessage({text: '请稍等下，正在努力为你解答！', type: 2}).then(() => {
                sendAsk(text)
            })
        } else {
            readMessage({text: '非常抱歉，老师还不能帮助你更多！', type: 2}).then(() => {
                showStartSpeek()
            })
        }
    })
}

function cancelAsk() {
    if (typeof (cancel) === 'function') {
        cancel()
    }
}

function onCancel() {
    cancelAsk()
    disabledSend.value = false
    isLoading.value = false
}

function onCancelSpeek() {
    if (audio && !audio.paused) {
        audio.pause()
    }
    showStartSpeek()
    speak_video.endSpeak()
}
//显示 开始对话
function showStartSpeek(){
    undisabledStartSpeek()
    isLoading.value = false
    isSpeekBtn.value = false
}
// 禁用对话
function disabledStartSpeek(){
    disabledSend.value = true
}
// 取消禁用对话
function undisabledStartSpeek(){
    disabledSend.value = false
}

//显示说话停止按钮
function showSpeekStopBtn(){
    isLoading.value = false
    isSpeekBtn.value = true
}
//显示请求停止按钮
function showSendStopBtn(){
    disabledStartSpeek()
    isLoading.value = true
    isSpeekBtn.value = false
}
function showLoading(){
    isLoading.value = true
}
function hideLoading(){
    isLoading.value = false
}
</script>

<style>
* {
    margin: 0;
    padding: 0;
}

/*滚动条样式*/
::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

::-webkit-scrollbar-track{background-color: transparent;}

::-webkit-scrollbar-track-piece {
    /* //滚动槽 */
    background-color: transparent;
    -webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
    height: 12px;
    background-color: #BDBDBD;
    -webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
    width: 12px;
    background-color: #BDBDBD;
    -webkit-border-radius: 6px;
}
/*滚动条鼠标移上去的样式*/
::-webkit-scrollbar-thumb:vertical:hover,
::-webkit-scrollbar-thumb:horizontal:hover {
    background-color: #808080;
}

#app {
    width: 100%;
    height: 100%;
    background: linear-gradient(45deg, rgb(94 107 238 / 44%), rgb(68 154 218 / 45%));
    position: absolute;
    left: 0;
}

.video {
    position: relative;
    z-index: 9;
}

.ai-container {
    position: absolute;
    height: 100%;
    width: 100%;
    overflow: hidden;
    left: 0;
    top: 0;
    background: #0297f0;
}

.ai-container.windows,
.ai-container.mac {
    top: 15px;
    max-width: 360px;
    max-height: 705px;
    left: 50%;
    transform: translateX(-50%);
    border-radius: 20px;
}

.bottom-content {
    position: absolute;
    width: 100%;
    height: 55%;
    bottom: 0;
    left: 0;
    background-image: linear-gradient(to top, rgb(35, 20, 80) 45%, rgb(35 35 80 / 46%) 88%, rgb(35 35 80 / 13%) 96%, #ffffff00);
    z-index: 999;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
}

.content-box {
    height: calc(100% - 158px);
    overflow: hidden;
}

.hj-button-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 20px;
    margin: 55px 0 45px;
    position: relative;
}

.hj-button {
    flex: 1;
    height: 50px;
    line-height: 50px;
    text-align: center;
    bottom: calc(12.5% - 25px);
    background: linear-gradient(to right, rgb(94, 107, 238), rgb(114, 97, 238));
    border-radius: 25px;
    cursor: pointer;
    color: #fff;
    font-size: 16px;
    margin-left: 30px;
}

.scroll-box {
    height: 100%;
    overflow-x: hidden;
    overflow-y: auto;
    padding: 0px 30px;
    color: #fff;
    position: relative;
}
.scroll-box::-webkit-scrollbar{
    width: 4px;
}
.call-box {
    position: fixed;
    height: 100%;
    width: 100%;
    left: 0;
    top: 0;
    z-index: 999;
    background: url(./imgs/bg.png) no-repeat #049af0;
    background-size: cover;
    display: flex;
    align-items: center;
    justify-content: space-around;
    flex-direction: column;
}

.call-img {
    width: 100px;
    height: 100px;
    overflow: hidden;
    border-radius: 50%;
    background: #6ed7ff;
}

.call-img-box {
    color: #ffffff;
    display: flex;
    flex-direction: column;
    align-items: center;
    font-size: 16px;
}

.call-img-box > span {
    margin-top: 15px;
}

#video-box {
    position: relative;
    z-index: 9;
    height: 100%;
    background-size: contain !important;
    background-repeat: no-repeat !important;
}

.logo {
    position: absolute;
    /* width: 114px; */
    height: 27px;
    left: 20px;
    top: 15px;
    z-index: 999;
}

.msg-icon {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    margin-right: 10px;
    position: relative;
    top: 6px;
}

.msg-icon:after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 5px;
    height: 5px;
    border-radius: 50%;
    transform: translate(-50%, -50%);
}

.msg-icon.green {
    background: rgb(70, 255, 255);
}
.msg-icon.gray {
    background: rgb(218, 223, 244);
}

.msg-icon.yellow {
    background: rgb(252 213 61)
}
.message-item {
    display: flex;
    align-items: flex-start;
    font-size: 14px;
    padding: 6px 0;
}

.message-item.type-1 {
    /*color: rgb(70, 255, 255);*/
}

.message-item .con {
    width: calc(100% - 20px);
    line-height: 1.5;
    word-spacing: normal;
    word-wrap: break-word;
    display: block;
    text-align: left;
    white-space: pre-wrap;
    word-break: break-all;
}

.hungup {
    height: 50px;
    width: 80px;
    border-radius: 25px;
    cursor: pointer;
    background: rgb(85, 86, 176);
    background-size: 34px auto;
    display: flex;
    align-items: center;
    justify-content: center;
}

.hungup img {
    width: 40px;
}

.hj-button.dui-hua.disabled {
    opacity: .5;
}

.waveview {
    position: absolute;
    top: -40px;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    height: 30px;
}

.scroll-box-content {
    padding: 30px 0;
}

.qs-container {
    position: absolute;
    bottom: 108px;
    width: 100%;
    z-index: 9999;
}

.qs-title {
    color: #ffffff;
    font-weight: bold;
    font-size: 14px;
    margin-left: 10px;
}

.qs-view {
    overflow-y: hidden;
    overflow-x: auto;
    white-space: nowrap;
    padding: 10px 10px;
}

.qs-item {
    color: #ffffff;
    font-size: 12px;
    display: inline-block;
    background: #393860;
    padding: 10px 16px;
    border-radius: 17px;
}

.qs-item + .qs-item {
    margin-left: 10px;
}
.ball-pulse > div:nth-child(1) {
    -webkit-animation: scale 0.75s -0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s -0.24s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
}


.ball-pulse > div:nth-child(2) {
    -webkit-animation: scale 0.75s -0.12s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s -0.12s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
}

.ball-pulse > div:nth-child(3) {
    -webkit-animation: scale 0.75s 0s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
    animation: scale 0.75s 0s infinite cubic-bezier(0.2, 0.68, 0.18, 1.08);
}

.ball-pulse > div {
    background-color: #fff;
    width: 8px;
    height: 8px;
    border-radius: 100%;
    margin: 2px;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
    display: inline-block;
}


@keyframes scale {
    30% {
        -webkit-transform: scale(0.3);
        transform: scale(0.3);
    }
    100% {
        -webkit-transform: scale(1);
        transform: scale(1);
    }
}
</style>
